/***************************************************************************\
 * Copyright (C) by Keio University
 * GLSLShader.cpp created in 09 2011.
 * Mail : fdesorbi@hvrl.ics.keio.ac.jp
 *
 * GLSLShader.cpp is part of the HVRL Engine Library.
 *
 * The HVRL Engine Library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * The HVRL Engine Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
\***************************************************************************/

#include "hvrl/opengl/GLSLShader.hpp"

#include "hvrl/common/Common.hpp"

#include <iostream>
#include <sstream>
#include <fstream>
#include <algorithm>


namespace hvrl
{

static const int MAX_LOG_STRING = 2048;
static char logstring[MAX_LOG_STRING];

namespace glsl
{

bool checkAvailability(void)
{

    if (glewIsSupported("GL_VERSION_2_0 GL_ARB_fragment_program GL_ARB_fragment_shader GL_ARB_shader_objects GL_ARB_shading_language_100 GL_ARB_vertex_program GL_ARB_vertex_shader"))
    {
        return true;
    }

    return false;
}


bool compile(GLuint object)
{

    if (object==0)
    {
        Log::add().error("glslCompile","Shader object is undefined");
        return false;
    }

    glCompileShader(object);

    int status = 0;
    glGetShaderiv(object, GL_COMPILE_STATUS, &status);
    if (status==0)
    {
            int length = 0;
            glGetShaderiv(object, GL_INFO_LOG_LENGTH, &length);
            if (length>0)
            {
                GLsizei minlength = std::min(MAX_LOG_STRING,length);
                glGetShaderInfoLog(object, minlength, 0, logstring);
                Log::add().error("glslCompile",(char*)logstring);
            }
        Log::add().error("glslCompile","Compilation of shader object has failed");
        return false;
    }
    return true;
}

bool compile(GLuint *objects,const size_t& nbobjects)
{

    if (objects==0)
    {
        Log::add().error("glslCompile","Array of shader objects is NULL");
        return false;
    }

    bool failure = false;
    std::string errmsg = "undefined";
    std::ostringstream out;
    for (unsigned int i = 0; i < nbobjects; ++i)
    {
        out << i;
        if (objects!=0)
        {

            glCompileShader(objects[i]);

            int status=0;
            glGetShaderiv(objects[i], GL_COMPILE_STATUS, &status);

            if (status==0)
            {
                errmsg = "Compilation of shader object " + out.str() + " has failed.";
                failure = true;
                    int length = 0;
                    glGetShaderiv(objects[i], GL_INFO_LOG_LENGTH, &length);
                    if (length>0)
                    {
                        GLsizei minlength = std::min(MAX_LOG_STRING,length);
                        glGetShaderInfoLog(objects[i], minlength, 0, logstring);
                        Log::add().error("glslCompile",logstring);
                    }
            }
        }
        else
        {
            errmsg = "Shader object " + out.str() + " is undefined.";
            failure = true;
        }
    }
    if (failure)
    {
        Log::add().error("glslCompile",errmsg);
        return false;
    }
    return true;
}

bool compile(const std::list<GLuint>& objects)
{

    if (objects.size()==0)
    {
        Log::add().error("glslCompile","List of shader objects is empty");
        return false;

    }

    bool failure = false;
    std::string errmsg = "undefined";
    std::ostringstream out;
    std::list<GLuint>::const_iterator it = objects.begin();
    int i = 0;
    while (it != objects.end())
    {
        out << i;
        if (*it!=0)
        {
            glCompileShader(*it);

            int status;
            glGetShaderiv(*it, GL_COMPILE_STATUS, &status);
            if (status==0)
            {
                errmsg = "Compilation of shader object " + out.str() + " has failed.";
                failure = true;
                    int length = 0;
                    glGetShaderiv(*it, GL_INFO_LOG_LENGTH, &length);
                    if (length>0)
                    {
                        GLsizei minlength = std::min(MAX_LOG_STRING,length);
                        glGetShaderInfoLog(*it, minlength, 0, logstring);
                        Log::add().debug("glslCompile",logstring);
                    }
            }
        }
        else
        {
            errmsg = "Shader object " + out.str() + " is undefined.";
            failure = true;
        }
        ++it;
        ++i;
    }
    if (failure)
    {
        Log::add().error("glslCompile",errmsg);
        return false;

    }
    return true;
}

GLuint link(GLuint* objects,const unsigned int& nb)
{

    if (objects==0)
    {
        Log::add().error("glslLink","Array of shader objects is NULL");
        return 0;
    }

    GLuint po = glCreateProgram();
    if (po==0)
    {
        Log::add().error("glslLink","The creation of the program object has failed");
        return 0;
    }

    for (unsigned int i = 0; i < nb; ++i)
    {
        if (objects[i]>0)
        {
            glAttachShader(po,objects[i]);
            GLenum err = glGetError();
            if (err != GL_NO_ERROR)
            {
                Log::add().error("glslLink",(char*)gluErrorString(err));
            }
        }
        else
        {
            Log::add().warning("glslLink","Shader object will not be used");
        }
    }

    glLinkProgram(po);

    GLenum  err = glGetError();
    if (err!=GL_NO_ERROR)
    {
        Log::add().error("glslLink",(char*)gluErrorString(err));
        return false;
    }
    int status;
    glGetProgramiv(po, GL_VALIDATE_STATUS, &status);
    if (status==0)
    {
        int length = 0;
        glGetProgramiv(po, GL_INFO_LOG_LENGTH, &length);
        if (length>0)
        {
            GLsizei minlength = std::min(MAX_LOG_STRING,length);
            glGetProgramInfoLog(po, minlength, 0, logstring);
            Log::add().error("glslLink",logstring);
        }
        Log::add().error("Link of shader objects has failed.","glsl::link");
        return 0;
    }
    return po;
}


GLuint link(const GLuint& object1, const GLuint& object2, bool debug)
{

    if (object1==0 || object2 ==0)
    {
        Log::add().error("glslLink","One of the shaders is NULL");
        return 0;
    }

    GLuint po = glCreateProgram();
    if (po==0)
    {
        Log::add().error("glslLink","The creation of the program object has failed");
        return 0;
    }

    glAttachShader(po,object1);
    GLenum err = glGetError();
    if (err != GL_NO_ERROR)
    {
        Log::add().error("glslLink",(char*)gluErrorString(err));
        return 0;
    }

    glAttachShader(po,object2);
    err = glGetError();
    if (err != GL_NO_ERROR)
    {
        Log::add().error("glslLink",(char*)gluErrorString(err));
        return 0;
    }

    glLinkProgram(po);

    int status;
    glGetProgramiv(po, GL_VALIDATE_STATUS, &status);
    if (status==0)
    {
        if (debug)
        {
            int length = 0;
            glGetProgramiv(po, GL_INFO_LOG_LENGTH, &length);
            if (length>0)
            {
                GLsizei minlength = std::min(MAX_LOG_STRING,length);
                glGetProgramInfoLog(po, minlength, 0, logstring);
                Log::add().error("glslLink",logstring);
            }
        }
        Log::add().error("glslLink","Link of shader objects has failed");
        return 0;
    }
    return po;
}


GLuint link(const std::list<GLuint>& objects)
{

    if (objects.size()==0)
    {
        Log::add().error("glslLink","List of shader objects is NULL");
        return 0;
    }

    GLuint po = glCreateProgram();
    if (po==0)
    {
        Log::add().error("glslLink","The creation of the program object has failed");
        return 0;
    }

    std::list<GLuint>::const_iterator it = objects.begin();
    int i = 0;
    while (it != objects.end())
    {
        if (*it>0)
        {
            glAttachShader(po,*it);
            GLenum err = glGetError();
            if (err != GL_NO_ERROR)
            {
                Log::add().error("glslLink",(char*)gluErrorString(err));
            }
        }
        else
        {
            Log::add().warning("glslLink", "Shader object  will not be used");
        }
        ++it;
        ++i;
    }

    glLinkProgram(po);

    int status;
    glGetProgramiv(po, GL_VALIDATE_STATUS, &status);
    if (status==0)
    {
        int length = 0;
        glGetProgramiv(po, GL_INFO_LOG_LENGTH, &length);
        if (length>0)
        {
            GLsizei minlength = std::min(MAX_LOG_STRING,length);
            glGetProgramInfoLog(po, minlength, 0, logstring);
            Log::add().error("glslLink", logstring);
        }
        Log::add().error("glslLink","Link of shader objects has failed");
        return 0;
    }
    return po;
}


GLuint loadShader(const char * filename)
{
    if (filename==0)
    {
        Log::add().error("glslLoadShader", "Filename is required");
        return 0;
    }

    std::string filestr(filename);
    std::transform(filestr.begin(), filestr.end(), filestr.begin(), ::tolower);

    if (filestr.find("vert") != std::string::npos || filestr.find("vertex") != std::string::npos)
    {
        return loadVertexShader(filename);
    }
    else
    {
        if (filestr.find("frag") != std::string::npos || filestr.find("fragment") != std::string::npos || filestr.find("pixel") != std::string::npos)
        {
            return loadFragmentShader(filename);
        }
        else
        {

            if (filestr.find("geom") != std::string::npos || filestr.find("geometry") != std::string::npos)
            {
#ifdef GL_ARB_geometry_shader4
                return loadGeometryShader(filename);
#else
                Log::add().error("glslLoadShader","Geometry shaders are not supported");
                return 0;
#endif

            }
            else
            {
                Log::add().error("glslLoadShader","Unable to define if the shader's type is vertex, geometry or fragment shader");
                return 0;
            }
        }
    }
    return 0;

}

GLchar * readFile(const char * filename)
{
    if (filename==0)
    {
        Log::add().error("readGLSLFile","Filename is required");
        return 0;
    }
    std::ifstream file(filename);
    if (!file)
    {
        Log::add().error("readGLSLFile","Unable to open file " + std::string(filename));
    }

    std::stringstream buffer;
    buffer << file.rdbuf();
    file.close();

    std::string s = buffer.str();

    // Required to avoid some memory corruptions
    GLchar * source = new GLchar[4*(buffer.str().size()/4+1)];

    unsigned int i;
    for (i =0; i < buffer.str().size(); ++i)
    {
        if (s[i]!='\0')
            source[i] = s[i];
        else
            source[i] = ' ';
    }
    source[i] = '\0';
    return source;
}

GLuint loadVertexShader(const char * filename)
{

    GLchar * source = readFile(filename);
    if (source == 0)
        return 0;

    GLuint so = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(so, 1,(const GLchar**) &source, 0);

    delete[] source;

    return so;
}

GLuint loadFragmentShader(const char * filename)
{

    GLchar * source = readFile(filename);
    if (source == 0)
        return 0;

    GLuint so = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(so, 1,(const GLchar**) &source, 0);

    delete[] source;

    return so;
}

#ifdef GL_ARB_geometry_shader4
GLuint loadGeometryShader(const char * filename)
{

    if (!GLEW_EXT_geometry_shader4)
    {
        Log::add().error("glslLoadGeometryShader", "OpenGL extension GL_EXT_geometry_shader4 is required");
        return 0;
    }

    GLchar * source = readFile(filename);
    if (source == 0)
        return 0;

    GLuint so = glCreateShader(GL_GEOMETRY_SHADER_EXT);
    glShaderSource(so, 1,(const GLchar**) &source, 0);

    delete[] source;

    return so;
}
#endif


}

}


